package memnet.graph;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;

public class Node {

	Object data;
	private EdgeMap outgoingEdges;
	private EdgeMap incomingEdges;
	
	public Node(Object data){
		this.data = data;
		outgoingEdges = new EdgeMap(this);
		incomingEdges = new EdgeMap(this);
	}
	
	public Object data(){
		return data;
	}
	
	boolean containsOutgoingEdge(Node other, Edge edge){
		return outgoingEdges.contains(other, edge.data);
	}
	
	boolean containsIncomingEdge(Node other, Edge edge){
		return incomingEdges.contains(other, edge.data);
	}
	
	Set<Edge> getOutEdges(Node other){
		return outgoingEdges.getEdges(other);
	}
	
	Edge getOutEdge(Node other, EdgeData data){
		return outgoingEdges.getEdge(other, data);
	}
	
	Set<Edge> getInEdges(Node other){
		return incomingEdges.getEdges(other);
	}
	
	Edge getInEdge(Node other, EdgeData data){
		return incomingEdges.getEdge(other, data);
	}
	
	Set<Edge> getAllEdges(Node other){
		Set<Edge> x = getInEdges(other);
		x.addAll(outgoingEdges.getEdges(other));
		return x;
	}
	
	public void setEdge(Node other, EdgeData ed, double weight){
		setEdge(other, (new Edge(this,other,weight,ed)));
	}
	
	/**
	 * Adds/modifies the value of an edge to this node.
	 * @param other
	 * @param edge
	 */
	void setEdge(Node other, Edge edge){
		if( edge.matchesSrcDst(this, other)){
			outgoingEdges.setEdge(other, edge);
			other.incomingEdges.setEdge(this, edge);
		}else if( edge.matchesSrcDst(other, this)){
			incomingEdges.setEdge(other, edge);
			other.outgoingEdges.setEdge(this, edge);
		}else{
			throw new GraphError("Bad edge: src/dst are incorrect");
		}
	}
	
	/**
	 * Removes all edges between both other and this node.
	 * @param other
	 * @param edge
	 */
	void removeEdge(Node other, Edge edge){
		if( edge.matchesSrcDst(this, other)){
			outgoingEdges.removeEdge(other, edge.data);
			other.incomingEdges.removeEdge(this, edge.data);		
		}else if( edge.matchesSrcDst(other, this)){
			incomingEdges.removeEdge(other, edge.data);
			other.outgoingEdges.removeEdge(this, edge.data);		
		}else{
			throw new GraphError("Bad edge: src/dst are incorrect");
		}
	}
	
	void removeEdge(Node other, EdgeData data){
		outgoingEdges.removeEdge(other, data);
		incomingEdges.removeEdge(other, data);
		other.outgoingEdges.removeEdge(this, data);
		other.incomingEdges.removeEdge(this, data);
		
	}
	
	/**
	 * Removes this node from other.
	 * @param other
	 */
	void disconnectOther(Node other){
		other.incomingEdges.disconnectOther(this);
		other.outgoingEdges.disconnectOther(this);
		this.incomingEdges.disconnectOther(other);
		this.outgoingEdges.disconnectOther(other);
	}
	
	/**
	 * Removes all connections other nodes in the graph
	 * have with this node.
	 */
	public void disconnect(){
		incomingEdges.disconnect();
		outgoingEdges.disconnect();
		incomingEdges.clear();
		outgoingEdges.clear();
	}
	
	/**
	 * Returns a set of the edges leaving this node.
	 * (This set is a shallow copy of the one used to
	 * internally hold the nodes)
	 * @return
	 */
	public Set<Edge> getOutgoingEdges(){
		return new HashSet<Edge>(outgoingEdges.getEdges());
	}
	
	public Set<Edge> getIncomingEdges(){
		return new HashSet<Edge>(incomingEdges.getEdges());
	}
	
	public String toString(){
		return "N{"+data+"}";
	}
}

class EdgeMap{
	Map<Node,Map<EdgeData,Edge>> edges;
	Node n;
	
	EdgeMap(Node n){
		this.n = n;
		edges = new Hashtable<Node,Map<EdgeData,Edge>>();	
	}
	
	public void setEdge(Node other, Edge e){
		if( e == null){
			throw new GraphError("Cannot add null edge");
		}
		Map<EdgeData, Edge> otherEdges = edges.get(other);
		if( otherEdges == null){
			otherEdges = new Hashtable<EdgeData,Edge>();
			edges.put(other, otherEdges);
		}
		otherEdges.put(e.data, e);
	}
	
	public Edge removeEdge(Node other, EdgeData data){
		Map<EdgeData, Edge> otherEdges = edges.get(other);
		if( otherEdges == null){
			throw new GraphError("Edge does not exist: "+other+", "+data);
		}
		Edge e =  otherEdges.remove(data);
		if( e == null){
			throw new GraphError("Edge does not exist: "+other+", "+data);		
		}
		return e;
	}
	
	public Edge getEdge(Node other, EdgeData data){
		Map<EdgeData, Edge> otherEdges = edges.get(other);
		if( otherEdges == null){
			throw new GraphError("Edge does not exist: "+other+", "+data);
		}
		Edge e =  otherEdges.get(data);
		if( e == null){
			throw new GraphError("Edge does not exist: "+other+", "+data);		
		}
		return e;
	}
	
	public boolean contains(Node other, EdgeData edata){
		Map<EdgeData, Edge> otherEdges = edges.get(other);
		if( otherEdges == null){
			return false;
		}
		return otherEdges.containsKey(edata);
	}
	
	public void clear(){
		edges.clear();
	}
	
	
	/**
	 * Removes all references to the other node.
	 * The other node's maps are NOT updated.
	 * @param other
	 */
	public void disconnectOther(Node other){
		edges.remove(other);
	}
	
	/**
	 * Removes this node from all its surrounding nodes.
	 * However, the references in this node are not updated. Use clear().
	 */
	public void disconnect(){
		for( Node other : edges.keySet()){
			other.disconnectOther(n);
		}
	}
	
	public Set<Edge> getEdges(Node other){
		Map<EdgeData, Edge> otherEdges = edges.get(other);
		if( otherEdges == null){
			throw new GraphError("Edges do not exist between "+n+", "+other);
		}
		
		return (new HashSet(otherEdges.values()));
	}
	
	public Set<Edge> getEdges(){
		HashSet<Edge> allEdges = new HashSet<Edge>();
		for( Map<EdgeData,Edge> edgeSet : edges.values()){
			allEdges.addAll(edgeSet.values());
		}
		return allEdges;
	}
}
